Skip to content

Commit f92f439

Browse files
committed
add kitsune.l10n app for handling content localization
1 parent 474d8a4 commit f92f439

27 files changed

+3565
-23
lines changed

docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ services:
6767
- postgres
6868
- redis
6969

70+
beat:
71+
build:
72+
context: .
73+
target: base
74+
command: celery -A kitsune beat -l info
75+
env_file: .env
76+
volumes:
77+
- ./:/app:delegated
78+
user: ${UID:-kitsune}
79+
depends_on:
80+
- postgres
81+
- redis
82+
7083
mailcatcher:
7184
image: schickling/mailcatcher
7285
ports:

kitsune/community/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.db.models.functions import Now
99
from elasticsearch_dsl import A
1010

11+
from kitsune.l10n.utils import SUMO_L10N_BOT_USERNAME
1112
from kitsune.products.models import Product
1213
from kitsune.search.documents import AnswerDocument, ProfileDocument
1314
from kitsune.users.models import ContributionAreas, User
@@ -145,6 +146,7 @@ def top_contributors_l10n(
145146

146147
users = (
147148
User.objects.filter(created_revisions__in=revisions, is_active=True)
149+
.exclude(username=SUMO_L10N_BOT_USERNAME)
148150
.annotate(query_count=Count("created_revisions"))
149151
.order_by(F("query_count").desc(nulls_last=True))
150152
.select_related("profile")

kitsune/l10n/__init__.py

Whitespace-only changes.

kitsune/l10n/admin.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from datetime import timedelta
2+
3+
from django import forms
4+
from django.conf import settings
5+
from django.contrib import admin
6+
from django.core.exceptions import ValidationError
7+
from django.core.validators import validate_slug
8+
from django.urls import reverse
9+
from django.shortcuts import redirect
10+
11+
from kitsune.l10n.models import MachineTranslationSettings
12+
from kitsune.l10n.utils import duration_string, parse_duration
13+
14+
15+
def require_one_of_days_hours_or_minutes(value):
16+
if "," in duration_string(value):
17+
raise ValidationError(
18+
(
19+
"Must be expressed as an integer number of only one of the "
20+
'following units of measurement: "days", "hours", or "minutes" '
21+
'(for example, "3 days" or "2 hours" or "1 minutes").'
22+
)
23+
)
24+
25+
26+
class SimpleDurationField(forms.DurationField):
27+
def prepare_value(self, value):
28+
if isinstance(value, timedelta):
29+
return duration_string(value)
30+
return value
31+
32+
def to_python(self, value):
33+
if value in self.empty_values:
34+
return None
35+
if isinstance(value, timedelta):
36+
return value
37+
try:
38+
value = parse_duration(str(value))
39+
except OverflowError:
40+
raise ValidationError(
41+
self.error_messages["overflow"].format(
42+
min_days=timedelta.min.days, max_days=timedelta.max.days
43+
),
44+
code="overflow",
45+
)
46+
if value is None:
47+
raise ValidationError(self.error_messages["invalid"], code="invalid")
48+
return value
49+
50+
51+
class MultipleSlugField(forms.Field):
52+
widget = forms.Textarea(attrs=dict(rows=3, placeholder="Enter each slug on a new line."))
53+
54+
def prepare_value(self, value):
55+
if isinstance(value, list):
56+
return "\n".join(value)
57+
return value
58+
59+
def to_python(self, value):
60+
if not value:
61+
return []
62+
63+
result, errors = [], []
64+
for slug in value.splitlines():
65+
if not slug.strip():
66+
continue
67+
try:
68+
validate_slug(slug)
69+
except ValidationError:
70+
if not errors:
71+
errors.append(
72+
"A valid slug consists of letters, numbers, underscores or hyphens."
73+
)
74+
errors.append(f"'{slug}' is not a valid slug.")
75+
else:
76+
result.append(slug)
77+
78+
if errors:
79+
raise ValidationError(errors)
80+
return result
81+
82+
83+
class MachineTranslationSettingsForm(forms.ModelForm):
84+
LANGUAGE_CHOICES = tuple(
85+
(lang, f"{settings.LOCALES[lang].english} ({lang})")
86+
for lang in settings.SUMO_LANGUAGES
87+
if lang not in ("xx", "en-US")
88+
)
89+
90+
heartbeat_period = SimpleDurationField(
91+
validators=[require_one_of_days_hours_or_minutes],
92+
)
93+
review_grace_period = SimpleDurationField()
94+
post_review_grace_period = SimpleDurationField(label="Post-review grace period")
95+
enabled_languages = forms.MultipleChoiceField(
96+
choices=LANGUAGE_CHOICES,
97+
widget=forms.CheckboxSelectMultiple,
98+
label="Languages enabled for machine translation",
99+
required=False,
100+
)
101+
restrict_to_slugs = MultipleSlugField(
102+
label="Restrict machine translation to these KB article slugs",
103+
required=False,
104+
)
105+
excluded_slugs = MultipleSlugField(
106+
label="Disable machine translation for these KB article slugs",
107+
required=False,
108+
)
109+
110+
class Meta:
111+
model = MachineTranslationSettings
112+
fields = "__all__"
113+
114+
115+
@admin.register(MachineTranslationSettings)
116+
class MachineTranslationSettingsAdmin(admin.ModelAdmin):
117+
list_display = (
118+
"is_enabled",
119+
"llm_name",
120+
"heartbeat_period",
121+
"review_grace_period",
122+
"post_review_grace_period",
123+
"enabled_languages",
124+
"restrict_to_slugs",
125+
"excluded_slugs",
126+
"restrict_to_approved_after",
127+
)
128+
129+
form = MachineTranslationSettingsForm
130+
131+
def has_add_permission(self, request):
132+
return False
133+
134+
def has_delete_permission(self, request, obj=None):
135+
return False
136+
137+
def changelist_view(self, request, extra_context=None):
138+
obj = MachineTranslationSettings.load()
139+
return redirect(reverse("admin:l10n_machinetranslationsettings_change", args=[obj.id]))

kitsune/l10n/apps.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.apps import AppConfig
2+
3+
4+
class L10nConfig(AppConfig):
5+
name = "kitsune.l10n"
6+
default_auto_field = "django.db.models.BigAutoField"
7+
8+
def ready(self):
9+
import kitsune.l10n.signals # noqa
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
Assume the role of an expert at translating software-related technical documents from {{ source_language }} to {{ target_language }}.
2+
{%- if example %}
3+
4+
Next, I'm going to provide you with what we'll call the "antecedent" example. The "antecedent" example consists of two parts. The first part will be a piece of {{ source_language }} text, and the second part will be the translation of that {{ source_language }} text into {{ target_language }}. Remember this "antecedent" example, because it will be needed later. The "antecedent" example will be delimited by <<<begin-antecedent-example>>> and <<<end-antecedent-example>>>.
5+
6+
<<<begin-antecedent-example>>>
7+
Here is the {{ source_language }} text of the "antecedent" example (delimited by <<<begin>>> and <<<end>>>):
8+
9+
<<<begin>>>
10+
{{ example.source_text|safe }}
11+
<<<end>>>
12+
13+
Here is the corresponding {{ target_language }} translation of the {{ source_language }} text of the "antecedent" example (delimited by <<<begin>>> and <<<end>>>):
14+
15+
<<<begin>>>
16+
{{ example.target_text|safe }}
17+
<<<end>>>
18+
<<<end-antecedent-example>>>
19+
{%- endif %}
20+
{%- if include_wiki_instructions %}
21+
22+
Next, remember the following definitions of new terms (delimited by <<<begin-term-definitions>>> and <<<end-term-definitions>>>):
23+
24+
<<<begin-term-definitions>>>
25+
Definition of "wiki-hook":
26+
A "wiki-hook" is a string that case-sensitively matches the entire regular expression that follows (delimited by <<<begin>>> and <<<end>>>, and specified using Python’s regular expression syntax in Python's raw string notation):
27+
28+
<<<begin>>>
29+
r"\[\[(Image|Video|V|Button|UI|Include|I|Template|T):.*?\]\]"
30+
<<<end>>>
31+
32+
Definition of "wiki-article-link":
33+
A "wiki-article-link" is a string that case-sensitively matches the entire regular expression that follows (delimited by <<<begin>>> and <<<end>>>, and specified using Python’s regular expression syntax in Python's raw string notation):
34+
35+
<<<begin>>>
36+
r"\[\[(?!Image:|Video:|V:|Button:|UI:|Include:|I:|Template:|T:)[^|]+?(?:\|(?P<description>.+?))?\]\]"
37+
<<<end>>>
38+
39+
Definition of "wiki-external-link":
40+
A "wiki-external-link" is a string that case-sensitively matches the entire regular expression that follows (delimited by <<<begin>>> and <<<end>>>, and specified using Python’s regular expression syntax in Python's string and raw string notation):
41+
42+
<<<begin>>>
43+
r"\[((mailto:|git://|irc://|https?://|ftp://|/)[^<>\]\[" + "\x00-\x20\x7f" + r"]*)\s*(?P<description>.*?)\]"
44+
<<<end>>>
45+
{%- if example %}
46+
47+
Definition of "antecedent-wiki-map":
48+
The "antecedent-wiki-map" is a Python dictionary built from the "antecedent" example provided earlier. A Python dictionary maps keys to their values. Each "wiki-hook", "wiki-article-link", and "wiki-external-link" in the English text of the "antecedent" example becomes a key in the "antecedent-wiki-map", and each key's value is its corresponding translation found in the {{ target_language }} text of the "antecedent" example.
49+
{%- endif %}
50+
<<<end-term-definitions>>>
51+
{%- endif %}
52+
53+
Next, remember the following "special instructions" (delimited by <<<begin-special-instructions>>> and <<<end-special-instructions>>>):
54+
55+
<<<begin-special-instructions>>>
56+
Special Instruction 1:
57+
The following case-sensitive strings of text, each delimited by quotes, must not be changed:
58+
- "Anonym"
59+
- "Bugzilla"
60+
- "Camino"
61+
- "Fakespot"
62+
- "Firebug"
63+
- "Firefox"
64+
- "Firefox for Android"
65+
- "Firefox for iOS"
66+
- "Firefox for Enterprise"
67+
- "Firefox Focus"
68+
- "Firefox Relay"
69+
- "Firefox Developer Edition"
70+
- "Firefox Friends"
71+
- "Firefox Nightly"
72+
- "Firefox OS"
73+
- "Firefox Rocket"
74+
- "Foxkeh"
75+
- "Lightbeam"
76+
- "MDN"
77+
- "MDN Plus"
78+
- "Minimo"
79+
- "Mozilla"
80+
- "Mozillians"
81+
- "Mozilla Communities"
82+
- "Mozilla Reps"
83+
- "Mozilla Webmaker"
84+
- "Mozilla Wordmark"
85+
- "Mozilla Wordmark + Symbol"
86+
- "Mozilla VPN"
87+
- "Mozilla Monitor"
88+
- "Pocket"
89+
- "Pontoon"
90+
- "QMO"
91+
- "SUMO"
92+
- "Sunbird"
93+
- "Sync"
94+
- "Thunderbird"
95+
- "Thunderbird for Android"
96+
- "View Source"
97+
- "VPN"
98+
- "XUL"
99+
- "Android"
100+
- "iOS"
101+
- "Linux"
102+
- "Mac"
103+
- "MacOS"
104+
- "Windows"
105+
{% if include_wiki_instructions -%}
106+
- "{note}"
107+
- "{/note}"
108+
- "{warning}"
109+
- "{/warning}"
110+
- "{/for}"
111+
- "__TOC__"
112+
113+
Special Instruction 2:
114+
Strings that case-sensitively match the entire regular expression (specified using Python’s regular expression syntax in raw string notation) r"\{(for|key|filepath) .*?\}" must not be changed.
115+
116+
Special Instruction 3:
117+
For strings that case-sensitively match the entire regular expression (specified using Python’s regular expression syntax in raw string notation) r"\{(?:button|menu|pref) (?P<description>.*?)\}", only translate the text within the named group "description", and keep the rest of the string unchanged.
118+
119+
{% if example -%}
120+
Special Instruction 4:
121+
Each "wiki-hook" must be translated as follows. First, check if the "wiki-hook" is a key within the "antecedent-wiki-map". If it is a key within the "antecedent-wiki-map", use its value from the "antecedent-wiki-map" as its translation. If it is not a key within the "antecedent-wiki-map", it must remain unchanged.
122+
123+
Special Instruction 5:
124+
Each "wiki-article-link" must be translated as follows. First, check if the "wiki-article-link" is a key within the "antecedent-wiki-map". If it is a key within the "antecedent-wiki-map", use its value from the "antecedent-wiki-map" as its translation. If it is not a key within the "antecedent-wiki-map", only translate the text within its named group "description", and keep the rest of the string unchanged.
125+
126+
Special Instruction 6:
127+
Each "wiki-external-link" must be translated as follows. First, check if the "wiki-external-link" is a key within the "antecedent-wiki-map". If it is a key within the "antecedent-wiki-map", use its value from the "antecedent-wiki-map" as its translation. If it is not a key within the "antecedent-wiki-map", only translate the text within its named group "description", and keep the rest of the string unchanged.
128+
{%- else -%}
129+
Special Instruction 4:
130+
Each "wiki-hook" must not be changed.
131+
132+
Special Instruction 5:
133+
For each "wiki-article-link", only translate the text within its named group "description", and keep the rest of the string unchanged.
134+
135+
Special Instruction 6:
136+
For each "wiki-external-link", only translate the text within its named group "description", and keep the rest of the string unchanged.
137+
{%- endif %}
138+
{%- endif %}
139+
<<<end-special-instructions>>>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Please translate the {{ source_language }} text (delimited by <<<begin>>> and <<<end>>>) into {{ target_language }}, by strictly obeying each of the following steps:
2+
3+
{% if example and include_wiki_instructions -%}
4+
Step 1: Build the "antecedent-wiki-map", and remember it.
5+
6+
Step 2: Compare the {{ source_language }} text you've been asked to translate with the {{ source_language }} text of the "antecedent" example, and determine which parts are the same and which parts are different.
7+
8+
Step 3: For each part that is the same, copy its corresponding translation from the {{ target_language }} text of the "antecedent" example.
9+
10+
Step 4: For each part that is different, freshly translate that part while obeying the "special instructions" defined earlier.
11+
12+
Step 5: Review your translation, replacing each "wiki-hook", "wiki-article-link", and "wiki-external-link", if it matches a key within the "antecedent-wiki-map", with its value within the "antecedent-wiki-map".
13+
14+
Step 6: In your response, include the "antecedent-wiki-map", describe what you did for each step, and delimit your resulting translation with {{ result_delimiter_begin|safe }} and {{ result_delimiter_end|safe }}.
15+
{%- elif example -%}
16+
Step 1: Compare the {{ source_language }} text you've been asked to translate with the {{ source_language }} text of the "antecedent" example, and determine which parts are the same and which parts are different.
17+
18+
Step 2: For each part that is the same, copy its corresponding translation from the {{ target_language }} text of the "antecedent" example.
19+
20+
Step 3: For each part that is different, freshly translate that part while obeying the "special instructions" defined earlier.
21+
22+
Step 4: In your response, include the "antecedent-wiki-map", describe what you did for each step, and delimit your resulting translation with {{ result_delimiter_begin|safe }} and {{ result_delimiter_end|safe }}.
23+
{%- else -%}
24+
Step 1: Obey the "special instructions" while translating.
25+
26+
Step 2: In your response, delimit your resulting translation with {{ result_delimiter_begin|safe }} and {{ result_delimiter_end|safe }}.
27+
{%- endif %}
28+
29+
<<<begin>>>
30+
{{ source_text|safe }}
31+
<<<end>>>

0 commit comments

Comments
 (0)